#!/usr/bin/env python3
# -*- coding： utf-8 -*-

import logging
import math
import queue as Q
import time
from collections import namedtuple
from enum import Enum
from random import choice as rdm_choice
from random import randint
from sys import exit
from algo.ratelimiter import TokenBucket

import pygame
from pygame.locals import *

logging.getLogger().setLevel(logging.INFO)


class Color(Enum):
    RED = (255, 0, 0)
    GREEN = (0, 255, 0)
    BLUE = (0, 0, 255)
    YELLOW = (255, 255, 0)
    BLACK = (0, 0, 0)


class Side(Enum):
    RED = 'red'
    WHITE = 'white'


Vector = namedtuple('Vector', ['x', 'y'])


class Direction(Enum):
    UP = Vector(0, -1)
    DOWN = Vector(0, 1)
    LEFT = Vector(-1, 0)
    RIGHT = Vector(1, 0)


class BaseItem(pygame.sprite.Sprite):
    cont = 1000

    def __init__(self, screen, image, living=1):
        pygame.sprite.Sprite.__init__(self)
        self.item_id = BaseItem._cout_incr()
        self.screen = screen
        self.image = image
        self.rect = self.image.get_rect()
        self.living = living

    @staticmethod
    def _cout_incr():
        BaseItem.cont += 1
        return BaseItem.cont

    def is_overlap(self, item):
        return self.rect.colliderect(item.rect)

    def display(self):
        if self.living > 0:
            self.screen.blit(self.image, self.rect)


class Explosion(BaseItem):
    explosion_img_file = 'img/explosion.png'
    blast_img_files = {
        1: "img/blast1.png",
        2: "img/blast2.png"
    }

    def __init__(self, screen, left, top, living=4):
        e_image = pygame.image.load(Explosion.blast_img_files[1]).convert_alpha()
        super().__init__(screen=screen, image=e_image, living=living)
        self.blast_centor_left = left
        self.blast_centor_top = top

    @staticmethod
    def _division(n1, n2):
        if n1 % n2 == 0:
            return n1 / n2
        else:
            return (n1 // n2) + 1

    def display(self):
        if self.living <= 0:
            return
        if self.living > 4:
            self.living = 4
        b_image = pygame.image.load(Explosion.blast_img_files[3 - Explosion._division(self.living, 2)]).convert_alpha()
        b_rect = b_image.get_rect()
        b_rect.left = self.blast_centor_left - b_rect.width / 2
        b_rect.top = self.blast_centor_top - b_rect.height / 2
        self.screen.blit(b_image, b_rect)
        self.living -= 1


class Missile(BaseItem):
    width = 4
    height = 4
    max_shoot_distance = 200

    def __init__(self, screen, left, top, direction=Direction.UP, speed=10, side=Side.RED, living=1):
        m_image = pygame.Surface([Missile.width, Missile.height])
        m_image.fill(Color.YELLOW.value)
        super().__init__(screen=screen, image=m_image, living=living)
        self.direction = direction
        self.speed = speed
        self.side = side
        self.rect.left = left
        self.rect.top = top
        self.shoot_distance = 0

    def move(self):
        if self.living <= 0:
            return
        self.rect.left = self.rect.left + self.direction.value.x * self.speed
        self.rect.top = self.rect.top + self.direction.value.y * self.speed
        self.shoot_distance += self.speed
        if (self.rect.left > TankMain.border_width
                or self.rect.left < 0
                or self.rect.top > TankMain.border_height
                or self.rect.top < 0
                or self.shoot_distance >= Missile.max_shoot_distance):
            self.living = 0

    def explosion(self):
        return Explosion(screen=self.screen, left=self.rect.left, top=self.rect.top)


class Tank(BaseItem):
    tank_img_file = {
        Side.WHITE: {
            Direction.UP: 'img/white-tank-U.png',
            Direction.DOWN: 'img/white-tank-D.png',
            Direction.LEFT: 'img/white-tank-L.png',
            Direction.RIGHT: 'img/white-tank-R.png'
        }, Side.RED: {
            Direction.UP: 'img/red-tank-U.png',
            Direction.DOWN: 'img/red-tank-D.png',
            Direction.LEFT: 'img/red-tank-L.png',
            Direction.RIGHT: 'img/red-tank-R.png'
        }}

    def __init__(self, screen, left, top, side=Side.RED, direction=Direction.UP, speed=5, moving=False, living=5,
                 shoot_rate=2):
        t_image = pygame.image.load(Tank.tank_img_file[side][direction]).convert_alpha()
        super().__init__(screen=screen, image=t_image, living=living)
        self.side = side
        self.direction = direction
        self.rect.left = left
        self.rect.top = top
        self.speed = speed
        self.moving = moving
        self.shoot_rate_limiter = TokenBucket(1, shoot_rate)

    def move(self):
        if self.living <= 0:
            return
        if self.moving:
            self.rect.left = self.rect.left + self.direction.value.x * self.speed
            self.rect.top = self.rect.top + self.direction.value.y * self.speed
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.left > TankMain.border_width - self.rect.width:
            self.rect.left = TankMain.border_width - self.rect.width
        if self.rect.top < 0:
            self.rect.top = 0
        if self.rect.top > TankMain.border_height - self.rect.height:
            self.rect.top = TankMain.border_height - self.rect.height

    def turn_direction(self, direction=Direction.UP):
        self.direction = direction
        self.image = pygame.image.load(Tank.tank_img_file[self.side][self.direction]).convert_alpha()

    def shoot(self):
        if self.shoot_rate_limiter.consume(1):
            m_left, m_top = self.rect.left + self.rect.width / 2, self.rect.top + self.rect.height / 2
            return Missile(self.screen, left=m_left, top=m_top, direction=self.direction, side=self.side)
        else:
            return None


class Brick(BaseItem):
    brick_img_file = 'img/brick.png'

    def __init__(self, screen, left, top, living=1):
        b_image = pygame.image.load(Brick.brick_img_file).convert_alpha()
        super().__init__(screen=screen, image=b_image, living=living)
        self.rect.left = left
        self.rect.top = top


class _RectWrapper(object):
    def __init__(self, rect):
        self.rect = rect


class Wall(object):
    def __init__(self, screen, left, top, thickness=1, length=4, living=1):
        _brick = Brick(screen=screen, left=0, top=0)
        b_width = _brick.rect.width
        b_height = _brick.rect.height
        self.brick_list = []
        for horizon_index in range(0, length):
            for vertical_index in range(0, thickness):
                self.brick_list.append(
                    Brick(screen, left=left + horizon_index * b_width, top=top + vertical_index * b_height,
                          living=living))

    def display(self):
        for brick in self.brick_list:
            brick.display()
        pygame.display.flip()

    def collide(self, item):
        for brick in self.brick_list:
            if Wall._overlaped(brick.rect, item.rect):
                return True, brick
        return False, None

    @staticmethod
    def _overlaped(rect1, rect2):
        if (rect1.left > rect2.left + rect2.width
                or rect1.top > rect2.top + rect2.height):
            return False
        elif (rect2.left > rect1.left + rect1.width
              or rect2.top > rect1.top + rect1.height):
            return False
        else:
            return True

    def rm_brick(self, brick):
        self.brick_list.remove(brick)


class Robot(object):
    class _TankWraper(object):
        def __init__(self, tank, priority=0):
            super().__init__()
            self.tank = tank
            self.priority = priority

        def __lt__(self, other):
            return self.priority < other.priority

    def __init__(self, item_manager):
        self.item_manager = item_manager

    @staticmethod
    def caculate_direction(src_pos, dest_pos):
        dx = dest_pos[0] - src_pos[0]
        dy = dest_pos[1] - src_pos[1]
        dest_directions = []
        if dx < 0:
            dest_directions.append(Direction.LEFT)
        elif dx > 0:
            dest_directions.append(Direction.RIGHT)
        else:
            pass

        if dy < 0:
            dest_directions.append(Direction.UP)
        elif dy > 0:
            dest_directions.append(Direction.DOWN)
        else:
            pass

        if not dest_directions:
            return rdm_choice([d for d in Direction])
        else:
            return rdm_choice(dest_directions)

    @staticmethod
    def _distance(rect1, rect2):
        return math.hypot(rect2.left - rect1.left, rect2.top - rect2.top)

    @staticmethod
    def _get_direction(my_tank, enemy_tanks):
        if enemy_tanks:
            tank_queue = Q.PriorityQueue()
            for e_tank in enemy_tanks:
                tank_queue.put(Robot._TankWraper(tank=e_tank,
                                                 priority=Robot._distance(my_tank.rect, e_tank.rect)))
            return Robot.caculate_direction(src_pos=my_tank.rect, dest_pos=tank_queue.get().tank.rect)
        else:
            return Robot.caculate_direction(src_pos=my_tank.rect,
                                            dest_pos=(TankMain.border_width / 2, TankMain.border_height / 2))

    def tank_behavior(self):
        white_tanks = [t for t in self.item_manager.tank_list if (t.side == Side.WHITE and t.living > 0)]
        for tank in filter(lambda t: t.side == Side.RED and t.living > 0, self.item_manager.tank_list):
            if ((tank.rect.left < 1)
                    or (tank.rect.left > (TankMain.border_width - tank.rect.width - 1))
                    or (tank.rect.top < 1)
                    or (tank.rect.top > (TankMain.border_height - tank.rect.height - 1))):
                tank.turn_direction(
                    direction=Robot._get_direction(my_tank=tank, enemy_tanks=white_tanks))
            if randint(0, 1000) < 30:
                tank.turn_direction(
                    direction=Robot._get_direction(my_tank=tank, enemy_tanks=white_tanks))
            if randint(0, 1000) < 50:
                missile = tank.shoot()
                if missile:
                    self.item_manager.missile_list.append(missile)

            rdm = randint(0, 1000)
            if rdm < 50:
                tank.moving = False
            elif rdm > 900:
                tank.moving = True
            else:
                pass


class ItemManager(object):
    def __init__(self):
        self.missile_list = []
        self.explosion_list = []
        self.tank_list = []
        self.wall_list = []
        self.robot = Robot(item_manager=self)

    def refresh_item(self):
        self.evaluate_tank()
        self.evaluate_missile()
        self.evaluate_explosion()
        self.evaluate_wall()
        self.robot.tank_behavior()

    def evaluate_missile(self):
        for missile in self.missile_list:
            if missile.living == 0:
                self.missile_list.remove(missile)
                del missile
            elif missile.living < 0:
                self.explosion_list.append(missile.explosion())
                self.missile_list.remove(missile)
                del missile
            else:
                missile.move()
                missile.display()

    def evaluate_explosion(self):
        for explosion in self.explosion_list:
            if explosion.living <= 0:
                self.explosion_list.remove(explosion)
                del explosion
            else:
                explosion.display()

    def evaluate_tank(self):
        for tank in self.tank_list:
            hit_missiles = [m for m in self.missile_list if (m.side != tank.side and (tank.is_overlap(m)))]
            tank.living -= len(hit_missiles)
            for missile in hit_missiles:
                missile.living = -1

            ItemManager.try_move(tank, self.wall_list)
            tank.display()
            if tank.living <= 0:
                logging.info('tank[id=%d, side=%s, living=%d] will be drop now',
                             tank.item_id, tank.side, tank.living)
                self.tank_list.remove(tank)
                del tank

    @staticmethod
    def try_move(tank, wall_list):
        origin_left, origin_top = tank.rect.left, tank.rect.top
        tank.move()
        left, top = tank.rect.left, tank.rect.top

        _delta_left = abs(left - origin_left)
        _delta_top = abs(top - origin_top)
        if _delta_left == 0 and _delta_top == 0:
            return

        if tank.direction == Direction.UP:
            _rect_wapper = _RectWrapper(rect=Rect(left + tank.rect.width / 2, top, 1, _delta_top))
        elif tank.direction == Direction.DOWN:
            _rect_wapper = _RectWrapper(
                rect=Rect(origin_left + tank.rect.width / 2, origin_top + tank.rect.height, 1, _delta_top))
        elif tank.direction == Direction.RIGHT:
            _rect_wapper = _RectWrapper(
                rect=Rect(origin_left + tank.rect.width, origin_top + tank.rect.height / 2, _delta_left, 1))
        else:
            _rect_wapper = _RectWrapper(rect=Rect(left, top + tank.rect.height / 2, _delta_left, 1))

        for wall in wall_list:
            _collided, _brick = wall.collide(_rect_wapper)
            if _collided:
                tank.rect.left = origin_left
                tank.rect.top = origin_top

    def evaluate_wall(self):
        for missile in self.missile_list:
            for wall in self.wall_list:
                is_collide, collide_brick = wall.collide(missile)
                if is_collide:
                    collide_brick.living -= 1
                    missile.living -= 1
        for wall in self.wall_list:
            for broken_brick in [brick for brick in wall.brick_list if brick.living <= 0]:
                wall.rm_brick(broken_brick)
                del broken_brick
            wall.display()

    def is_game_over(self):
        if not [t for t in self.tank_list if (t.side == Side.WHITE)]:
            return True, Side.RED
        if not [t for t in self.tank_list if (t.side == Side.RED)]:
            return True, Side.WHITE
        return False, None


class TankMain(object):
    border_width = 640
    border_height = 480
    background_image_filename = 'img/background.png'
    red_tank_num = 6

    def __init__(self):
        pygame.init()
        pygame.font.init()
        pygame.display.set_caption("Tank War")
        self.screen = pygame.display.set_mode((TankMain.border_width, TankMain.border_height), 0, 32)
        self.background = pygame.image.load(TankMain.background_image_filename).convert()
        self.rect = self.background.get_rect()
        self.screen.blit(self.background, self.rect)
        self.text_font = pygame.font.Font(None, 30)
        pygame.display.update()
        self.itemManager = ItemManager()
        logging.info('init tank game finished!')

    def _hint_if_terminal(self):
        is_over, win_side = self.itemManager.is_game_over()
        if is_over:
            text_surface = self.text_font.render('Game Over: winner is {}'.format(win_side.value), False,
                                                 (255, 255, 255))
            self.screen.blit(text_surface, (0, 0))

    def start_game(self):
        logging.info('start game...')
        tank1 = Tank(self.screen, left=TankMain.border_width * 0.33, top=TankMain.border_height / 2, side=Side.WHITE,
                     direction=Direction.UP, speed=5)
        tank2 = Tank(self.screen, left=TankMain.border_width * 0.66, top=TankMain.border_height / 2, side=Side.WHITE,
                     direction=Direction.UP, speed=5)
        self.itemManager.tank_list.append(tank1)
        self.itemManager.tank_list.append(tank2)
        for i in range(0, TankMain.red_tank_num):
            self.itemManager.tank_list.append(Tank(self.screen,
                                                   left=randint(0, TankMain.border_width),
                                                   top=randint(0, TankMain.border_height),
                                                   side=Side.RED,
                                                   direction=[d for d in Direction][randint(0, len(Direction) - 1)],
                                                   moving=True, speed=5, shoot_rate=3))

        self.itemManager.wall_list.append(
            Wall(self.screen, left=0, top=TankMain.border_height * 0.3, thickness=3, length=21, living=3))

        self.itemManager.wall_list.append(
            Wall(self.screen, left=TankMain.border_width * 0.4, top=0, thickness=16, length=2, living=3))

        while True:
            self.screen.blit(self.background, self.rect)
            self.itemManager.refresh_item()
            self._hint_if_terminal()

            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    logging.info('game terminated!')
                    exit()
                if event.type == KEYDOWN:
                    if event.key == K_ESCAPE:
                        pygame.quit()
                        logging.info('game terminated!')
                        exit()
                    # key listener for tank1
                    elif event.key == K_w:
                        tank1.turn_direction(Direction.UP)
                        tank1.moving = True
                    elif event.key == K_s:
                        tank1.turn_direction(Direction.DOWN)
                        tank1.moving = True
                    elif event.key == K_a:
                        tank1.turn_direction(Direction.LEFT)
                        tank1.moving = True
                    elif event.key == K_d:
                        tank1.turn_direction(Direction.RIGHT)
                        tank1.moving = True
                    elif event.key == K_j:
                        if tank1.living > 0:
                            missile = tank1.shoot()
                            if missile:
                                self.itemManager.missile_list.append(missile)

                    # key listener for tank2
                    elif event.key == K_UP:
                        tank2.turn_direction(Direction.UP)
                        tank2.moving = True
                    elif event.key == K_DOWN:
                        tank2.turn_direction(Direction.DOWN)
                        tank2.moving = True
                    elif event.key == K_LEFT:
                        tank2.turn_direction(Direction.LEFT)
                        tank2.moving = True
                    elif event.key == K_RIGHT:
                        tank2.turn_direction(Direction.RIGHT)
                        tank2.moving = True
                    elif event.key == K_l:
                        if tank2.living > 0:
                            missile = tank2.shoot()
                            if missile:
                                self.itemManager.missile_list.append(missile)

                    else:
                        pass
                if event.type == KEYUP:
                    if event.key in (K_a, K_w, K_d, K_s):
                        tank1.moving = False
                    if event.key in (K_LEFT, K_UP, K_RIGHT, K_DOWN):
                        tank2.moving = False

            pygame.display.update()
            time.sleep(0.05)


if __name__ == '__main__':
    TankMain().start_game()
